@file:OptIn(ExperimentalStdlibApi::class)

package com.harth.flutter_ns_ble

import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothManager
import android.content.Context
import android.util.Log
import androidx.core.content.getSystemService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.timeout
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.ble.TimeoutableRequest
import no.nordicsemi.android.ble.WriteRequest
import no.nordicsemi.android.ble.callback.WriteProgressCallback
import no.nordicsemi.android.ble.data.Data
import no.nordicsemi.android.ble.exception.RequestFailedException
import no.nordicsemi.android.ble.ktx.state.ConnectionState
import no.nordicsemi.android.ble.ktx.stateAsFlow
import no.nordicsemi.android.ble.ktx.suspend
import java.io.File
import java.io.FileNotFoundException
import java.io.RandomAccessFile
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.UUID
import kotlin.math.ceil
import kotlin.time.Duration.Companion.seconds


class PeripheralManager(private val context: Context) : ProvidePeripheralEventStreamHandler(),
    PeripheralMethod {
    var eventSink: PigeonEventSink<PeripheralEvent>? = null

    override fun onListen(p0: Any?, sink: PigeonEventSink<PeripheralEvent>) {
        super.onListen(p0, sink)
        eventSink = sink
    }

    private val peripherals = mutableMapOf<String, Peripheral>()
    private val scopes = mutableMapOf<String, CoroutineScope>()

    val notifyFlow = MutableSharedFlow<PeripheralNotifyEvent>(replay = 0)
    fun getPeripheral(address: String): Peripheral {
        return peripherals[address] ?: Peripheral(context).apply {
            peripherals[address] = this
            getScope(address).launch {
                this@apply.stateAsFlow().collect {
                    eventSink?.success(
                        PeripheralStateEvent(
                            address = address,
                            state = when (it) {
                                ConnectionState.Connecting -> PeripheralState.CONNECTING
                                is ConnectionState.Disconnected -> PeripheralState.DISCONNECTED
                                ConnectionState.Disconnecting -> PeripheralState.DISCONNECTING
                                ConnectionState.Initializing -> PeripheralState.CONNECTING
                                ConnectionState.Ready -> PeripheralState.READY
                            }
                        )
                    )
                }
            }
        }
    }

    fun getScope(address: String): CoroutineScope {
        return scopes[address] ?: CoroutineScope(SupervisorJob() + Dispatchers.Main).apply {
            scopes[address] = this
        }
    }

    private val adapter by lazy {
        context.getSystemService<BluetoothManager>()!!.adapter
    }

    private val connectionRequest = mutableMapOf<String, TimeoutableRequest>()

    fun callSink(event: PeripheralEvent) {
        mainScope.launch {
            eventSink?.success(event)
        }

    }

    override fun ensureInitialized(address: String, callback: (Result<Boolean>) -> Unit) {
        getPeripheral(address)
        callback(Result.success(true))
    }

    override fun connect(
        address: String,
        callback: (Result<Boolean>) -> Unit
    ) {
        val bluetoothDevice = adapter.getRemoteDevice(address)
        getPeripheral(address).connect(bluetoothDevice)
            .timeout(20_000)
            .retry(2)
            .fail { device, status ->
                callback(Result.success(false))
            }
            .then {
                connectionRequest.remove(address)
            }
            .done {
                callback(Result.success(true))
            }
            .apply {
                connectionRequest[address] = this
            }
            .enqueue()
    }

    override fun disconnect(
        address: String,
        callback: (Result<Boolean>) -> Unit
    ) {
        connectionRequest[address]?.cancel()
        getPeripheral(address).disconnect().enqueue()
    }

    override fun write(
        address: String,
        cmd: CMD,
        value: ByteArray?,
        callback: (Result<Boolean>) -> Unit
    ) {
        getScope(address).launch {
            try {
                val packet = BlePacket.fromCmd(cmd.toInt(), data = value)
                getPeripheral(address).writeCharacteristic(packet.byteArray)
                    .suspend()
                callback(Result.success(true))
            } catch (e: Exception) {
                Log.e(
                    "PeripheralManager",
                    "write address:$address, cmd:${
                        cmd.toInt().toString(16)
                    }, value:${value?.toHexString()}"
                )
                e.printStackTrace()
                callback(Result.failure(e))
            }
        }
    }

    private val dfuJobs = mutableMapOf<String, Job>()


    override fun startDFU(
        address: String,
        filePath: String,
        callback: (Result<Boolean>) -> Unit
    ) {
        dfuJobs[address]?.cancel()
        dfuJobs[address] = getScope(address).launch {
            try {
                getPeripheral(address).let { peripheral ->
                    dfu(address, peripheral, filePath)
                }

            } catch (e: Exception) {
                e.printStackTrace()
                callback(Result.failure(e))
                callSink(
                    DFUStateEvent(
                        address = address,
                        state = DFUState.COMPLETED,
                        stateCode = -1,
                        errorMsg = "${e.message}"
                    )
                )
            }

        }
        dfuJobs[address]?.invokeOnCompletion {
            dfuJobs.remove(address)
        }
    }


    override fun cancelDFU(
        address: String,
        callback: (Result<Boolean>) -> Unit
    ) {

        dfuJobs[address]?.cancel()
        callSink(
            DFUStateEvent(
                address = address,
                state = DFUState.CANCELED
            )
        )
    }

    private val b1DeviceSyncDataJobs = mutableMapOf<String, Job>()


    override fun startB1SyncData(
        address: String,
        filePath: String?,
        deleteBeforeWrite: Boolean?,
        callback: (Result<String?>) -> Unit
    ) {

        b1DeviceSyncDataJobs[address]?.cancel()
        b1DeviceSyncDataJobs[address] = getScope(address).launch {
            try {
                getPeripheral(address).let { peripheral ->
                    syncData(context, address, peripheral, filePath, deleteBeforeWrite)
                }

            } catch (e: Exception) {
                e.printStackTrace()
                callback(Result.failure(e))
                callSink(
                    B1SyncDataEvent(
                        address = address,
                        state = B1SyncDataState.COMPLETED,
                        stateCode = -1,
                        errorMsg = "${e.message}"
                    )
                )
            }

        }
        b1DeviceSyncDataJobs[address]?.invokeOnCompletion {
            dfuJobs.remove(address)
        }
    }

    override fun cancelB1SyncData(
        address: String,
        callback: (Result<Boolean>) -> Unit
    ) {
        getScope(address).launch {
            getPeripheral(address).write(CMD.STOP_SYNC)
        }
        b1DeviceSyncDataJobs[address]?.cancel()
        callSink(
            B1SyncDataEvent(
                address = address,
                state = B1SyncDataState.CANCELED
            )
        )
    }

    companion object {
        val SERVICE_UUID: UUID = UUID.fromString("de5bf728-d711-4e47-af26-65e3012a5dc7")
        val CHARACTERISTIC_NOTIFY_UUID: UUID =
            UUID.fromString("de5bf729-d711-4e47-af26-65e3012a5dc7")
        val CHARACTERISTIC_WRITE_UUID: UUID =
            UUID.fromString("de5bf72a-d711-4e47-af26-65e3012a5dc7")
    }

    private var logEventSink: PigeonEventSink<LogEvent>? = null

    val logEventHandler = object : ProvideLogEventStreamHandler() {
        override fun onListen(p0: Any?, sink: PigeonEventSink<LogEvent>) {
            logEventSink = sink
        }

        override fun onCancel(p0: Any?) {
            logEventSink = null
        }
    }
    val mainScope = MainScope()

    inner class Peripheral(context: Context) : BleManager(context) {

        override fun getMinLogPriority(): Int {
            // Use to return minimal desired logging priority.
            return Log.VERBOSE
        }

        override fun log(priority: Int, message: String) {
            // Log from here.
//            Log.println(priority, "Peripheral", message)
            mainScope.launch {
                logEventSink?.success(
                    LogEvent(
                        priority = priority.toLong(),
                        message = message
                    )
                )
            }
        }

        private var characteristicForNotify: BluetoothGattCharacteristic? = null
        private var characteristicForWrite: BluetoothGattCharacteristic? = null

        override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
            // Here obtain instances of your characteristics.
            // Return false if a required service has not been discovered.
            val fluxCapacitorService = gatt.getService(SERVICE_UUID)
            if (fluxCapacitorService != null) {
                characteristicForNotify =
                    fluxCapacitorService.getCharacteristic(CHARACTERISTIC_NOTIFY_UUID)
                characteristicForWrite =
                    fluxCapacitorService.getCharacteristic(CHARACTERISTIC_WRITE_UUID)
            }
            return characteristicForNotify != null && characteristicForWrite != null
        }

        override fun initialize() {
            setNotificationCallback(characteristicForNotify)
                .merge(BlePacketDataMerger())
                .with { device, data ->
                    data.value?.let { raw ->
                        BlePacket.fromByteArray(raw)?.let {
                            PeripheralNotifyEvent(
                                address = bluetoothDevice?.address ?: "",
                                cmd = it.cmd.toCMD(),
                                data = it.data,
                                raw = raw
                            ).apply {
                                getScope(address).launch {
                                    notifyFlow.emit(this@apply)
                                }
                                callSink(this)
                            }

                        }
                    }
                }

            enableNotifications(characteristicForNotify)
                .enqueue()
            requestMtu(513)
                .enqueue()
        }

        fun writeCharacteristic(data: ByteArray): WriteRequest {
            return writeCharacteristic(
                characteristicForWrite,
                data,
                BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
            )
                .split()
        }

        suspend fun write(cmd: CMD, data: ByteArray? = null) {
            val packet = BlePacket.fromCmd(cmd.toInt(), data)
            writeCharacteristic(packet.byteArray)
                .suspend()
        }

        suspend fun request(cmd: CMD, data: ByteArray?): PeripheralNotifyEvent = withContext(
            Dispatchers.Main
        ) {
            val resp = async {
                notifyFlow.filter {
                    it.cmd == cmd
                }.first()
            }
            val packet = BlePacket.fromCmd(cmd.toInt(), data)
            writeCharacteristic(packet.byteArray)
                .suspend()
            resp.await()
        }

        override fun onServicesInvalidated() {
            // This method is called when the services get invalidated, i.e. when the device
            // disconnects.
            // References to characteristics should be nullified here.
            characteristicForNotify = null
            characteristicForWrite = null
        }

    }
}

fun main() {
//    flow {
//        emit(1)
//        delay(100)
//        emit(2)
//        delay(100)
//        emit(3)
//        delay(1000)
//        emit(4)
//    }.timeout(1.seconds).catch { exception ->
//        if (exception is TimeoutCancellationException) {
//            // Catch the TimeoutCancellationException emitted above.
//            // Emit desired item on timeout.
//            emit(-1)
//        } else {
//            // Throw other exceptions.
//            throw exception
//        }
//    }.onEach {
//        delay(300) // This will not cause a timeout
//    }
}